commonlibsse_ng\rel\version/
mod.rs

1// C++ Original code
2// - ref: https://github.com/SARDONYX-forks/CommonLibVR/blob/ng/include/REL/Version.h
3// SPDX-FileCopyrightText: (C) 2018 Ryan-rsm-McKenzie
4// SPDX-License-Identifier: MIT
5//
6// SPDX-FileCopyrightText: (C) 2025 SARDONYX
7// SPDX-License-Identifier: Apache-2.0 OR MIT
8
9mod win_api;
10
11pub use win_api::{FileVersionError, get_file_version};
12
13/// Represents a 4-part version number.
14///
15/// In binding, [`Copy`] is inherited, but it is omitted to avoid implicit copying in for loops, etc.
16///
17/// # Example
18/// ```
19/// use commonlibsse_ng::rel::version::Version;
20///
21/// let ver = Version::new(1, 6, 1170, 0);
22/// assert_eq!(ver.major(), 1);
23/// assert_eq!(ver.to_string(), "1.6.1170.0");
24/// ```
25#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
26pub struct Version {
27    /// Internal representation of the version as a 4-element array.
28    ///
29    /// - Index 0: Major version
30    /// - Index 1: Minor version
31    /// - Index 2: Patch version
32    /// - Index 3: Build number
33    ///
34    /// This field is private, and access should be done through provided methods.
35    _impl: [u16; 4],
36}
37
38impl Version {
39    /// Create a empty version.
40    ///
41    /// # Examples
42    /// ```
43    /// use commonlibsse_ng::rel::version::Version;
44    ///
45    /// assert_eq!(Version::default_const(), Version::new(0, 0, 0, 0));
46    /// ```
47    #[inline]
48    pub const fn default_const() -> Self {
49        Self::new(0, 0, 0, 0)
50    }
51
52    /// Creates a new `Version` from four components.
53    ///
54    /// # Example
55    /// ```
56    /// use commonlibsse_ng::rel::version::Version;
57    ///
58    /// let ver = Version::new(1, 2, 3, 4);
59    /// assert_eq!(ver.major(), 1);
60    /// ```
61    #[inline]
62    pub const fn new(major: u16, minor: u16, patch: u16, build: u16) -> Self {
63        Self { _impl: [major, minor, patch, build] }
64    }
65
66    /// Parses a version string at compile time.
67    ///
68    /// # Panics
69    /// Errors are made under the following conditions.
70    ///
71    /// - When there is no number after a point.
72    /// - If there are more than 4 numbers.
73    /// - When there is a non-numeric character (other than a dot).
74    ///
75    /// # Examples
76    /// ```rust
77    /// use commonlibsse_ng::rel::version::{Version, VersionParseError};
78    ///
79    /// assert_eq!(Version::from_str_const("1.2.3"), Version::new(1, 2, 3, 0));
80    ///
81    /// // Panics
82    /// // assert_eq!(Version::from_str_const_("1.2.3.4.5")); // Too many numbers
83    /// // assert_eq!(Version::from_str_const("1.2.f.4.5")); // Invalid char `f`
84    /// // assert_eq!(Version::from_str_const("1.2.")); // Missing number
85    /// ```
86    #[inline]
87    pub const fn from_str_const(version: &str) -> Self {
88        let mut parts = [0_u16; 4];
89        let mut idx = 0;
90        let mut num = 0;
91        let mut has_digit = false;
92
93        let bytes = version.as_bytes();
94        let len = bytes.len();
95        let mut i = 0;
96        while i < len {
97            let b = bytes[i];
98            if b == b'.' {
99                if idx >= 4 {
100                    panic!("Expected at most 4 parts, but got too many parts.");
101                }
102                parts[idx] = num;
103
104                num = 0;
105                idx += 1;
106                has_digit = false;
107            } else if b.is_ascii_digit() {
108                num = num * 10 + (b - b'0') as u16;
109                has_digit = true;
110            } else {
111                panic!("Expected a number but got invalid character.");
112            }
113            i += 1;
114        }
115
116        if has_digit {
117            if idx >= 4 {
118                panic!("Expected at most 4 parts, but got too many parts.");
119            }
120            parts[idx] = num;
121        } else {
122            panic!("Expected numbers after the dots, but got none in parts");
123        }
124
125        Self { _impl: parts }
126    }
127
128    /// Returns the major version component.
129    ///
130    /// # Examples
131    /// ```
132    /// use commonlibsse_ng::rel::version::Version;
133    ///
134    /// let v = Version::new(1, 2, 3, 4);
135    /// assert_eq!(v.major(), 1);
136    /// ```
137    #[inline]
138    pub const fn major(&self) -> u16 {
139        self._impl[0]
140    }
141
142    /// Returns the minor version component.
143    ///
144    /// # Examples
145    /// ```
146    /// use commonlibsse_ng::rel::version::Version;
147    ///
148    /// let v = Version::new(1, 2, 3, 4);
149    /// assert_eq!(v.minor(), 2);
150    /// ```
151    #[inline]
152    pub const fn minor(&self) -> u16 {
153        self._impl[1]
154    }
155
156    /// Returns the patch version component.
157    ///
158    /// # Examples
159    /// ```
160    /// use commonlibsse_ng::rel::version::Version;
161    ///
162    /// let v = Version::new(1, 2, 3, 4);
163    /// assert_eq!(v.patch(), 3);
164    /// ```
165    #[inline]
166    pub const fn patch(&self) -> u16 {
167        self._impl[2]
168    }
169
170    /// Returns the build version component.
171    ///
172    /// # Examples
173    /// ```
174    /// use commonlibsse_ng::rel::version::Version;
175    ///
176    /// let v = Version::new(1, 2, 3, 4);
177    /// assert_eq!(v.build(), 4);
178    /// ```
179    #[inline]
180    pub const fn build(&self) -> u16 {
181        self._impl[3]
182    }
183
184    /// Packs the version into a 32-bit integer.
185    /// # Examples
186    /// ```
187    /// use commonlibsse_ng::rel::version::Version;
188    ///
189    /// let v = Version::new(1, 2, 3, 4);
190    /// assert_eq!(v.pack(), 16908340);
191    /// ```
192    #[inline]
193    pub const fn pack(&self) -> u32 {
194        ((self._impl[0] as u32 & 0xFF) << 24)
195            | ((self._impl[1] as u32 & 0xFF) << 16)
196            | ((self._impl[2] as u32 & 0xFFF) << 4)
197            | (self._impl[3] as u32 & 0xF)
198    }
199
200    /// Unpacks a 32-bit integer into a `Version`.
201    #[inline]
202    pub const fn unpack(packed: u32) -> Self {
203        Self {
204            _impl: [
205                ((packed >> 24) & 0xFF) as u16,
206                ((packed >> 16) & 0xFF) as u16,
207                ((packed >> 4) & 0xFFF) as u16,
208                (packed & 0xF) as u16,
209            ],
210        }
211    }
212
213    /// Gets the inner parts.
214    ///
215    /// # Example
216    /// ```
217    /// # use commonlibsse_ng::rel::version::Version;
218    /// let v = Version::new(1, 2, 3, 4);
219    /// assert_eq!(v.parts(), [1, 2, 3, 4]);
220    /// ```
221    #[inline]
222    pub const fn parts(&self) -> [u16; 4] {
223        self._impl
224    }
225
226    /// To address library file name string.
227    ///
228    /// # Example
229    /// ```
230    /// # use commonlibsse_ng::rel::version::Version;
231    /// let v = Version::new(1, 2, 3, 4);
232    /// assert_eq!(v.to_address_library_string(), "1-2-3-4");
233    /// ```
234    #[inline]
235    pub fn to_address_library_string(&self) -> String {
236        let [major, minor, patch, build] = self._impl;
237        format!("{major}-{minor}-{patch}-{build}")
238    }
239}
240
241impl Default for Version {
242    fn default() -> Self {
243        Self::default_const()
244    }
245}
246
247impl core::fmt::Display for Version {
248    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
249        let major = self._impl[0];
250        let minor = self._impl[1];
251        let patch = self._impl[2];
252        let build = self._impl[3];
253        write!(f, "{major}.{minor}.{patch}.{build}",)
254    }
255}
256
257impl core::ops::Index<usize> for Version {
258    type Output = u16;
259
260    #[inline]
261    fn index(&self, index: usize) -> &Self::Output {
262        &self._impl[index]
263    }
264}
265
266impl core::ops::IndexMut<usize> for Version {
267    #[inline]
268    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
269        &mut self._impl[index]
270    }
271}
272
273impl core::str::FromStr for Version {
274    type Err = VersionParseError;
275
276    #[inline]
277    fn from_str(version: &str) -> Result<Self, Self::Err> {
278        let mut parts = [0_u16; 4];
279        let mut idx = 0;
280        let mut num = 0;
281        let mut has_digit = false;
282
283        let bytes = version.as_bytes();
284        let len = bytes.len();
285        let mut i = 0;
286        while i < len {
287            let b = bytes[i];
288            if b == b'.' {
289                if idx >= 4 {
290                    return Err(VersionParseError::TooManyParts { parts: idx });
291                }
292                parts[idx] = num;
293
294                num = 0;
295                idx += 1;
296                has_digit = false;
297            } else if b.is_ascii_digit() {
298                num = num * 10 + (b - b'0') as u16;
299                has_digit = true;
300            } else {
301                return Err(VersionParseError::InvalidCharacter { character: b as char });
302            }
303            i += 1;
304        }
305
306        if has_digit {
307            if idx >= 4 {
308                return Err(VersionParseError::TooManyParts { parts: idx });
309            }
310            parts[idx] = num;
311        } else {
312            return Err(VersionParseError::MissingNumber { part: idx });
313        }
314
315        Ok(Self { _impl: parts })
316    }
317}
318
319#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, snafu::Snafu)]
320pub enum VersionParseError {
321    /// Expected at most 4 parts, but got {parts} parts
322    TooManyParts { parts: usize },
323
324    /// Expected a number but got invalid character: `{character}`
325    InvalidCharacter { character: char },
326
327    /// Expected numbers after the dots, but got none in part {part}
328    MissingNumber { part: usize },
329}
330
331#[cfg(test)]
332mod tests {
333    use super::*;
334
335    #[test]
336    fn test_version_ord() {
337        let v1 = Version::new(1, 2, 3, 4);
338        let v2 = Version::new(1, 2, 3, 5);
339        let v3 = Version::new(2, 0, 0, 0);
340        let v4 = Version::new(1, 2, 3, 4);
341
342        assert!(v1 < v2);
343        assert!(v2 > v1);
344        assert!(v3 > v1);
345        assert!(v1 == v4);
346    }
347
348    #[test]
349    fn test_from_str() {
350        use core::str::FromStr as _;
351
352        assert_eq!(Version::from_str("1.2.3.4"), Ok(Version::new(1, 2, 3, 4)));
353        assert_eq!(Version::from_str("1.2.3"), Ok(Version::new(1, 2, 3, 0)));
354
355        assert_eq!(
356            Version::from_str("1.2.3.4.5"),
357            Err(VersionParseError::TooManyParts { parts: 4 }) // 0 based index. got 5 length
358        );
359        assert_eq!(
360            Version::from_str("1.2.f.4.5"),
361            Err(VersionParseError::InvalidCharacter { character: 'f' })
362        );
363        assert_eq!(Version::from_str("1.2."), Err(VersionParseError::MissingNumber { part: 2 }));
364    }
365}